Skip to content

feat: enable log forwarding through otel collector#186

Open
florentianayuwono wants to merge 22 commits intomainfrom
feat/log-forwarding-isd-5146
Open

feat: enable log forwarding through otel collector#186
florentianayuwono wants to merge 22 commits intomainfrom
feat/log-forwarding-isd-5146

Conversation

@florentianayuwono
Copy link
Copy Markdown
Collaborator

@florentianayuwono florentianayuwono commented Apr 22, 2026

What this PR does

Add action to allow workflow authors to opt in to forwarding specific log files from self-hosted GitHub runners to Loki through the OpenTelemetry Collector snap.

Why we need it

So that workflow author can opt-in to forwarding specific log files from their job to Grafana, and view application logs alongside system metrics without changing their existing logging approach.

Checklist

  • Changes comply with the project's coding standards and guidelines (see CONTRIBUTING.md and STYLE.md)
  • CONTRIBUTING.md has been updated upon changes to the contribution/development process (e.g. changes to the way tests are run)
  • Technical author has been assigned to review the PR in case of documentation changes (usually *.md files)
  • I updated docs/changelog.md with user-relevant changes
  • I used AI to assist with preparing this PR
  • I added or updated tests as needed (unit and integration)
  • If integration test modules are used: I updated the workflow configuration
    (e.g., in .github/workflows/integration_tests.yaml, ensure the modules list is correct)
  • If this PR involves a Grafana dashboard: I added a screenshot of the dashboard
  • If this PR involves Terraform: terraform fmt passes and tflint reports no errors
  • If this PR involves Rockcraft: I updated the version

florentianayuwono and others added 9 commits April 22, 2026 22:53
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…v v8 (#182)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix(dashboard): remove dead override hiding job queue time series

The "Job queue time" panel had a leftover hideSeriesFrom override that
excluded every series except one specific named expression. Combined
with the panel's current query (which already aggregates with sum by
(le)), the override hid all data, leaving an empty chart.

The override is obsolete now that the query collapses every label
except le into a single combined histogram, so removing it restores
visualisation without any other change.

* ci: pin astral-sh/setup-uv to v8.1.0

The astral-sh/setup-uv repository does not publish a floating v8 major
tag (only v8.0.0 and v8.1.0 specific tags exist), so referencing @v8
fails to resolve and breaks the workflow on every PR.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Christopher Bartz <christopher.bartz@canonical.com>
* chore(docs): update contributing guidelines (charmkeeper)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(docs): build errors

* chore: clarify PR checklist and remove unnecessary item

* chore(docs): revert changes in CONTRIBUTING.md

* fix(docs): whoops we're ignoring the changelog

* fix: update pr checklist to incorporate previous items, remove duplicate items

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new composite GitHub Action that can opt workflows into forwarding selected log files from self-hosted runners to Loki via an OpenTelemetry Collector snap.

Changes:

  • Introduces actions/enable-log-forwarding composite action that writes an otelcol config fragment and restarts the collector.
  • Adds unit tests and a CI workflow (including a self-hosted smoke test) for the new action.
  • Documents usage in a new how-to guide and records the change in the docs changelog.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
docs/how-to/enable-log-forwarding.md New how-to page describing how to use the log-forwarding action and query hints.
docs/changelog.md Adds a 2026-04-22 entry documenting the new action.
actions/enable-log-forwarding/tests/test_enable_log_forwarding.py Unit tests for parsing inputs, endpoint resolution, exporter detection, and config generation.
actions/enable-log-forwarding/enable-log-forwarding.py Action implementation that generates an otelcol config fragment, installs it, and restarts the collector.
actions/enable-log-forwarding/action.yaml Composite action definition and inputs wiring to the Python script.
.github/workflows/enable_log_forwarding_action_tests.yaml CI workflow to run unit tests and a self-hosted smoke test for the action.

Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread actions/enable-log-forwarding/action.yaml Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md
Comment thread actions/enable-log-forwarding/action.yaml Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread docs/how-to/enable-log-forwarding.md
Copy link
Copy Markdown
Contributor

@erinecon erinecon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a how-to guide with this PR! Some initial comments and questions

Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/enable-log-forwarding.md
Copy link
Copy Markdown
Member

@yanksyoon yanksyoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial review done, thank you!

Comment thread .github/workflows/enable_log_forwarding_action_tests.yaml Outdated
Comment thread .github/workflows/enable_log_forwarding_action_tests.yaml Outdated
Comment thread .github/workflows/enable_log_forwarding_action_tests.yaml Outdated
Comment thread actions/enable_log_forwarding/action.yaml
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
Comment thread actions/enable_log_forwarding/tests/test_enable_log_forwarding.py
florentianayuwono and others added 4 commits April 23, 2026 10:21
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erin Conley <erin.conley@canonical.com>
Comment thread docs/how-to/index.rst
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update the README? Usually the front page is the one the users are looking at (at least I do) cc @erinecon

I know the current state of the README is not up to date, it still marks the repo as WIP. Any opinions @erinecon ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also update the README?

Yes we should update the README, but my question would be whether those updates fall under the scope
of this PR or if we should update the README in a follow-up PR.

I know the current state of the README is not up to date, it still marks the repo as WIP.

Is the repo still WIP? We should continue to clearly indicate that, but regardless, I still think we should update the README.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think readme should be done in a separate pr / jira ticket

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this, would it be helpful if I opened an issue into the repo, or would you just prefer the Jira ticket?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erinecon yes i think we need separate jira ticket for readme (cc @cbartz)



def build_config(files, resolved_endpoint, exporter_already_exists):
attrs = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we use a templated string ? so that we can more quickly understand the generated config and avoid the requirement to read the python code to get a bigger picture

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i added a j2 file for this :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using a template for JSON is not a very good idea, as you need to consider escape characters if the input contains something like " or \n when using a template to compose a JSON file. It's much cleaner and more error-proof to use Python's json library. This also saves us from installing the jinja2 package.

Comment thread actions/enable-log-forwarding/enable-log-forwarding.py Outdated
florentianayuwono and others added 3 commits April 23, 2026 20:02
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Comment thread docs/how-to/enable-log-forwarding.md Outdated
florentianayuwono and others added 4 commits April 24, 2026 01:04
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread docs/how-to/index.rst
Copy link
Copy Markdown
Member

@yanksyoon yanksyoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more nits, almost there! Thanks @florentianayuwono :)

Comment thread .github/workflows/enable_log_forwarding_action_tests.yaml
Comment thread .github/workflows/enable_log_forwarding_action_tests.yaml
Comment on lines +11 to +13
MODULE_PATH = (
pathlib.Path(__file__).resolve().parent.parent / "enable_log_forwarding.py"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be a better way to load the module? I'm wondering if we can safely assume that the test will be run from project root and import using the path relative to project root.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since load_module function for a test seems like a bit of an overkill.

MODULE = load_module(MODULE_PATH)


class TestEnableLogForwarding(unittest.TestCase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we approach our tests with pytest and in functional testing style please? I think this is probably to align with our team's style guidelines :)

class TestEnableLogForwarding(unittest.TestCase):
"""Test suite for parsing, endpoint resolution, exporter detection, and config generation."""

def test_parse_files_into_list(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the tests have arrange/act/assert pattern in functional testing paradigm? This is to align with our team's style guidelines :)

Comment on lines +21 to +22
SNAP_CMD = shutil.which("snap")
SUDO_CMD = shutil.which("sudo")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should hardcode the paths for these - so that the executables are not dynamically loaded, for security purposes.

Comment on lines +215 to +217
mkdir_result = run_as_root(
"mkdir", "-p", CONFIG_DIR # create directory if it doesn't exist
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be using pathlib?

Comment on lines +227 to +229
install_result = run_as_root(
"install", "-m", "0644", tmp_path, config_path # rw-r--r-- permissions
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we prefer Pythonic way to install (copy and chmod) rather than calling install command?

Copy link
Copy Markdown
Collaborator Author

@florentianayuwono florentianayuwono left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review session from weii

@@ -0,0 +1,115 @@
# Copyright 2026 Canonical Ltd.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the folder name, we should use - instead of _

"start_at": "end"
}
},
{{ exporters_section }}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do a test to see if it won't cause a conflict if the exporter is not optional and there are two config files has the same exporter

def check_exporter_exists() -> bool:
"""Return whether the configured exporter is already defined in collector config files."""
# Check for " otlp_grpc:" exporter definition
pattern = f"^ {re.escape(EXPORTER_NAME)}:[ \t]*$"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently this way of checking is not very reliable, it's either:

  1. not needed, if there is no problem with same exporters exist in multiple config files
  2. use a python yaml library to analyze the yaml file content (ex: PyYAML)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the exporter is started, maybe there's a command line that dumps some information about the current configuration. If that's not started there's maybe a configuration validation command that can give information.

("github.repository", os.getenv("GITHUB_REPOSITORY", "unknown")),
("github.runner.name", os.getenv("RUNNER_NAME", "unknown")),
("github.workflow", os.getenv("GITHUB_WORKFLOW", "unknown")),
("github.job.id", os.getenv("GITHUB_JOB", "unknown")),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finally:
os.unlink(tmp_path)

logger.info("Wrote log-forwarding collector config to: %s", config_path)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log the final content of the config file, if wanna be fancy can use github action logging format https://docs.github.com/en/enterprise-server@3.18/actions/reference/workflows-and-actions/workflow-commands#grouping-log-lines

capture_output=True,
check=False,
)
if snap_list_result.returncode != 0:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case it's not installed just install it

## Prerequisites

- Use a self-hosted Linux runner.
- Install the `opentelemetry-collector` snap on the runner.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't be needed if we install it for the runner in the action steps

- Ensure the workflow can update `/etc/otelcol/config.d` with root privileges.

## Provide inputs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure that you do not include any files that may contain sensitive information so that these info are not forwarded

Copy link
Copy Markdown
Contributor

@erinecon erinecon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve the documentation changes 👍 since there's code-based changes in this PR, I didn't provide an official approval.

Spotted a couple more nits + responded to a Copilot comment

Comment thread docs/how-to/enable-log-forwarding.md
Comment thread docs/how-to/enable-log-forwarding.md Outdated
Comment thread docs/how-to/index.rst
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this, would it be helpful if I opened an issue into the repo, or would you just prefer the Jira ticket?

Comment thread docs/how-to/index.rst
Co-authored-by: Erin Conley <erin.conley@canonical.com>
inputs:
files:
description: |
Newline or comma separated list of file paths or glob patterns to forward.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, avoid double "or", so
Newline, comma separated list of file paths or

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the code, we support Comma separated, or list separated AND globbing. So you can have multiple globbing

files:
description: |
Newline or comma separated list of file paths or glob patterns to forward.
Example: /var/log/chrony/*.log
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide an example for each supported content (newline, comma separated, and glob)

otlp-endpoint:
description: |
Optional OTLP/gRPC endpoint for upstream OpenTelemetry Collector logs export.
When not set, the action falls back to ACTION_OTEL_EXPORTER_OTLP_ENDPOINT.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be more specific about where this env var comes from like:
injected automatically by self-hosted github runners provided in Canonical

Comment on lines +19 to +20
config-file-name:
description: File name for the generated collector fragment.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be specific about the usecase, renaming might helpful for priority mainly if they want this file to be evaluated before or after some other rules

Comment thread tox.ini
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bandit is missing
And a pyproject.toml needs to be added to configure the linting /mypy rules that we want (see for example https://github.com/canonical/wireguard-gateway-operator/blob/main/pyproject.toml)

"""Parse comma/newline-separated file patterns into a normalized list."""
entries = []
# Split on commas or newlines, and strip whitespace. Ignore empty entries.
for item in re.split(r"[,\n]", files_input):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the regext as a constant

Comment on lines +268 to +270
if not files:
logger.error("Input 'files' must contain at least one path or glob.")
sys.exit(1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For coherence, in the other methods the guarding is done inside the private methods, so move this to parse_files_into_list


def resolve_endpoint() -> str:
"""Resolve OTLP endpoint from explicit input, then workflow fallback variable."""
# If INPUT_OTLP_ENDPOINT is not set, fall back to ACTION_OTEL_EXPORTER_OTLP_ENDPOINT
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be in the docstring

def check_exporter_exists() -> bool:
"""Return whether the configured exporter is already defined in collector config files."""
# Check for " otlp_grpc:" exporter definition
pattern = f"^ {re.escape(EXPORTER_NAME)}:[ \t]*$"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the exporter is started, maybe there's a command line that dumps some information about the current configuration. If that's not started there's maybe a configuration validation command that can give information.


resolved_endpoint = resolve_endpoint()
exporter_already_exists = check_exporter_exists()
validate_exporter_configuration(resolved_endpoint, exporter_already_exists)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic can be moved here, the main method is not that big

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you do the guarding in the build_config method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants